# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import calendar
import datetime
import functools
import pathlib
import json

import pytz
import dateutil.parser
import absl
from absl import flags, app

from coin.exchange.bitmex.kr_rest.constants import coin_list
import coin.exchange.bitmex.kr_rest.native_public_client as npc
from coin.exchange.base.kr_rest.futures_product import (gen_full_symbol, gen_short_symbol)
from coin.exchange.bitmex.kr_rest.currency import (
    from_native_currency,
    BitmexCurrency,
)

flags.DEFINE_bool('use_bitmex_cached_info', False, 'Use Bitmex cached info.')


class DatetimeEncoder(json.JSONEncoder):
  def default(self, obj):
    if isinstance(obj, datetime.datetime):
      return {
          "_type": "datetime",
          "value": str(obj),
      }
    return super().default(obj)


class DatetimeDecoder(json.JSONDecoder):
  def __init__(self, *args, **kwargs):
    super().__init__(object_hook=self.object_hook, *args, **kwargs)

  def object_hook(self, obj):
    if '_type' not in obj:
      return obj

    type = obj['_type']
    if type == 'datetime':
      return dateutil.parser.parse(obj['value'])


def get_last_friday(year, month):
  cal = calendar.Calendar(4)  # Make Friday the first day.
  week_list = cal.monthdayscalendar(year, month)
  fridays = [week[0] for week in week_list if week[0] != 0]
  return datetime.date(year, month, fridays[-1])


def get_etc7d_expiry_friday():
  utc_today = datetime.datetime.utcnow()
  week_day = utc_today.weekday()
  if week_day == 4:  # If today is Friday.
    if utc_today.now().time() < datetime.time(12):
      return utc_today.date()
    else:
      return (utc_today + datetime.timedelta(days=7)).date()
  else:
    days = 4 - week_day if week_day < 4 else 11 - week_day
    return (utc_today + datetime.timedelta(days=days)).date()


def get_expiry_from_code(code):
  assert len(code) == 3, 'Invalid code: %s' % code
  month_code = "FGHJKMNQUVXZ"
  month = month_code.index(code[0]) + 1  # Will raise if not found.
  year = int('20' + code[1:])
  assert 2010 <= year <= 2030, 'Invalid year %s' % year
  return get_last_friday(year, month)


def gen_expiry_code(expiry: str):
  month_code = "FGHJKMNQUVXZ"
  expiry_date = datetime.datetime.strptime(expiry, '%Y%m%d')
  assert 2010 <= expiry_date.year <= 2030
  code = '%s%02d' % (month_code[expiry_date.month - 1], (expiry_date.year - 2000))
  return code


# Parse native symbol that has the form of |currency pair code + expiry code|
# or |base + expiry code|. For example, XBJH18, XBUM18, NEOH18 etc.
def parse_normal_futures_product(native_symbol: str):
  assert isinstance(native_symbol, str), \
      'Invalid input type %s' % type(native_symbol)

  expiry_code = native_symbol[-3:]
  res = {
      'native_symbol': native_symbol,
      'expiry': get_expiry_from_code(expiry_code),
  }

  if native_symbol.startswith('XBJ'):
    res['base'] = 'BTC'
    res['quote'] = 'JPY'
  elif native_symbol.startswith('XBK'):
    res['base'] = 'BTC'
    res['quote'] = 'KRW'
  elif native_symbol.startswith('XBT'):
    res['base'] = 'BTC'
    res['quote'] = 'USD'
  elif native_symbol.startswith('ETHUSD'):
    res['base'] = 'ETH'
    res['quote'] = 'USD'
  elif native_symbol.startswith('BCHUSD'):
    res['base'] = 'BCH'
    res['quote'] = 'USD'
  else:
    underlying_native_symbol = native_symbol[:-3]
    if underlying_native_symbol.endswith('USDT'):
      res['base'] = underlying_native_symbol[:-4]
      if res['base'] not in coin_list:
        raise ValueError('Unexpected native symbol: %s' % native_symbol)
      res['quote'] = 'USDT'
    else:
      res['base'] = native_symbol[:-3]
      if not (res['base'] in coin_list and res['base'] != 'ETC'):
        raise ValueError('Unexpected native symbol: %s' % native_symbol)
      res['quote'] = 'BTC'
  return res


def parse_native_symbol(native_symbol):
  res = {
      'native_symbol': native_symbol,
      'expiry': 'PERPETUAL',
  }
  if native_symbol == 'XBTUSD':
    res['base'] = 'BTC'
    res['quote'] = 'USD'
  elif native_symbol == 'ETHUSD':
    res['base'] = 'ETH'
    res['quote'] = 'USD'
  elif native_symbol == 'BCHUSD':
    res['base'] = 'BCH'
    res['quote'] = 'USD'
  elif native_symbol == 'XRPUSD':
    res['base'] = 'XRP'
    res['quote'] = 'USD'
  elif native_symbol == 'LTCUSD':
    res['base'] = 'LTC'
    res['quote'] = 'USD'
  elif native_symbol == 'LINKUSDT':
    res['base'] = 'LINK'
    res['quote'] = 'USDT'
  elif native_symbol == 'XBTJPY':
    res['base'] = 'BTC'
    res['quote'] = 'JPY'
  elif native_symbol == 'XBTKRW':
    res['base'] = 'BTC'
    res['quote'] = 'KRW'
  elif native_symbol == 'ETHXBT':
    res['base'] = 'ETH'
    res['quote'] = 'BTC'
  elif native_symbol == 'ETC7D':
    res['base'] = 'ETC'
    res['quote'] = 'BTC'
    res['expiry'] = get_etc7d_expiry_friday()
  elif native_symbol == 'XBT7D_U110':
    res['base'] = 'BTC'
    res['quote'] = 'BTC'
    res['expiry'] = get_etc7d_expiry_friday()
  else:  #
    res = parse_normal_futures_product(native_symbol)
  return res


def gen_native_symbol(base: BitmexCurrency, quote: BitmexCurrency, expiry: str):
  if expiry == 'PERPETUAL':
    if base.native_symbol == 'XBT' and quote.native_symbol == 'USD':
      return base.native_symbol + quote.native_symbol
    if base.native_symbol == 'ETH' and quote.native_symbol == 'USD':
      return base.native_symbol + quote.native_symbol
    if base.native_symbol == 'BCH' and quote.native_symbol == 'USD':
      return base.native_symbol + quote.native_symbol
    if base.native_symbol == 'XRP' and quote.native_symbol == 'USD':
      return base.native_symbol + quote.native_symbol
    raise ValueError('Invalid perpetual contract! (%s,%s,%s)' % (base, quote, expiry))
  elif base.native_symbol == 'ETC' and quote.native_symbol == 'XBT':
    assert expiry == get_etc7d_expiry_friday().strftime('%Y%m%d'), \
        'Invalid expiry date!'
    return 'ETC7D'
  else:
    if quote.native_symbol == 'XBT':
      return base.native_symbol + gen_expiry_code(expiry)
    elif base.native_symbol == 'XBT' and quote.native_symbol == 'USD':
      return 'XBT' + gen_expiry_code(expiry)
    elif base.native_symbol == 'XBT' and quote.native_symbol == 'JPY':
      return 'XBJ' + gen_expiry_code(expiry)
    elif base.native_symbol == 'XBT' and quote.native_symbol == 'KRW':
      return 'XBK' + gen_expiry_code(expiry)
    elif base.native_symbol == 'ETH' and quote.native_symbol == 'USD':
      return 'ETHUSD' + gen_expiry_code(expiry)
    else:
      raise ValueError('Unknown product %s,%s,%s' % (base, quote, expiry))


# Return value is cached.
@functools.lru_cache(maxsize=1)
def gen_bitmex_futures_product_list():
  exchange = 'Bitmex'

  product_info_list = fetch_exchange_product_info()
  assert product_info_list
  res = {}
  utcnow = datetime.datetime.now(tz=pytz.utc)
  for prod_info in product_info_list:
    # Ensure that it is a finacial future currency product.
    # FFCCSX and FFWCSX is CFI code. Perpetual contract's CFI code is FFWCSX.
    if prod_info['typ'] in ['FFCCSX', 'FFWCSX']:
      if prod_info['expiry'] and prod_info['expiry'] < utcnow:
        continue
      expiry = prod_info['expiry'] or 'PERPETUAL'
      tick_size = float(prod_info['tickSize'])
      native_symbol = prod_info['symbol']
      quote = from_native_currency(prod_info['quoteCurrency'])
      base = parse_native_symbol(native_symbol)['base']
      short_symbol = gen_short_symbol(base=base, quote=quote, expiry=expiry)
      full_symbol = gen_full_symbol(base=base,
                                    quote=quote,
                                    expiry=expiry,
                                    exchange=exchange,
                                    native_symbol=native_symbol)
      res[short_symbol] = {
          'base': base,
          'quote': quote,
          'short_symbol': short_symbol,
          'full_symbol': full_symbol,
          'native_symbol': native_symbol,
          'expiry': expiry,
          'tick_size': tick_size,
      }
    elif prod_info['typ'] in [
        "FFICSX", "FMXXS", "FXXXS", "MRCXXX", "MRIXXX", "MRRXXX", "OCECCS", "OPECCS"
    ]:
      pass
    else:
      raise ValueError('Unknown product type code %s' % prod_info['typ'])

  for native_symbol in ['XBTJPY', 'XBTKRW']:
    parsed_result = parse_native_symbol(native_symbol)
    base = parsed_result['base']
    quote = parsed_result['quote']
    expiry = parsed_result['expiry']
    short_symbol = gen_short_symbol(base=base, quote=quote, expiry=expiry)
    full_symbol = gen_full_symbol(base=base,
                                  quote=quote,
                                  expiry=expiry,
                                  exchange=exchange,
                                  native_symbol=native_symbol)
    res[short_symbol] = {
        'base': base,
        'quote': quote,
        'short_symbol': short_symbol,
        'full_symbol': full_symbol,
        'native_symbol': native_symbol,
        'expiry': expiry,
        'tick_size': 1,  # Hack, but doesn't matter since only available in testnet.
    }

  assert res, 'Future product not found!'
  return res


def fetch_exchange_product_info():
  try:
    if flags.FLAGS.use_bitmex_cached_info:
      info_path = pathlib.Path(__file__).parent.joinpath('exchange_product_info.json')
      return json.loads(info_path.read_text(), cls=DatetimeDecoder)
  except absl.flags._exceptions.UnparsedFlagAccessError:
    pass
  except FileNotFoundError:
    pass

  client = npc.BitmexNativePublicClient(timeout=5, test=False)
  return client.get_instrument_open().msg


@functools.lru_cache(maxsize=1024)
def try_get_symbol_info(product: str):
  assert isinstance(product, str), product
  futures_product_list = gen_bitmex_futures_product_list()
  if product in futures_product_list:
    return futures_product_list[product]

  for symbol_info in futures_product_list.values():
    if product in [
        symbol_info['native_symbol'], symbol_info['short_symbol'], symbol_info['full_symbol']
    ]:
      return symbol_info
  raise ValueError('Unknown product %s' % product)


def main(_):
  info = fetch_exchange_product_info()
  info_path = pathlib.Path(__file__).parent.joinpath('exchange_product_info.json')
  with info_path.open('w') as outfile:
    outfile.write(json.dumps(info, indent=2, cls=DatetimeEncoder))

  with info_path.open() as infile:
    info1 = json.load(infile, cls=DatetimeDecoder)

  assert (info == info1)


if __name__ == '__main__':
  app.run(main)
